import type { AObject } from "./AObject";
import type { ARoutine, RoutineArguments, RoutineBase } from "./ARoutine";
import type { Agent } from "./Agent";
import type { Character } from "./Character";
import * as Debug from "./Debug";
import type { ScheduledRoutine } from "./Definitions";
import type { NarrativeContext } from "./NarrativeContext";
import { ObjectSelector, parseObjectSelector, type ObjectSelectorPolicy } from "./Selector";
import type { ScheduledProcess, TypedScheduledProcess } from "./WorldBase";

/** Parses natural language numbers. Can handle any grammatically correct expressions 
 *  up to 千万. Returns NaN on error. */
export function parseNumber(s: string): number {
    let num = Number.parseFloat(s);
    if (!isNaN(num)) return num;

    const digits: {[x: string]: number} = {
        '零': 0, '一': 1, '两': 2, '二': 2, '三': 3, '四': 4, 
        '五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
    };
    const multipliers: {[x: string]: number} = {
        '十': 10, '百': 100, '千': 1000, '万': 10000
    };
    let total = 0, current = 0, temp = 0;
    for (let ch of s) {
        let d = digits[ch], m = multipliers[ch];
        if (d != undefined) {
            if (temp != 0) return NaN;
            temp = d;
        } else if (m != undefined) {
            if (m > total && m > current) {
                current = Math.max(total + current + temp, 1) * m;
                total = 0; temp = 0;
            } else {
                if (m < current) {
                    total += current;
                    current = 0;
                }
                if (temp == 0) temp = 1;
                current = (current + temp) * m;
                temp = 0;
            }
        } else return NaN;
    }
    return total + current + temp;
}

export type CommandConfig = {
    character: Character,
    defaultRange: () => AObject[],
    notFoundMsg: (orig: string, name?: string) => void,
    defaultsToMsg: (objs: AObject[], name?: string) => void,
    conditionNotMetMsg: (obj: AObject, name?: string) => void,
    reportError: (err: unknown) => void
}

export type SelectorCondition = [
    (obj: AObject, args: VerbArguments) => boolean, 
    ((obj: AObject, ag: Agent, args: VerbArguments) => void) | null];

function _getQuery(
    matches: RegExpMatchArray, 
    source: string[] | ((x: RegExpMatchArray) => string | undefined))
{
    if (Array.isArray(source)) {
        Debug.assert(source.length > 0);
        if (matches.groups) for (let s of source)
            if (matches.groups[s])
                return matches.groups[s];
        return undefined;
    } else return source(matches);
}

interface ActionParameter {
    source: string[] | ((x: RegExpMatchArray) => string | undefined);
    defaultsTo: string;
    createArgument(matches: RegExpMatchArray, config: CommandConfig, 
        args: VerbArguments, context: NarrativeContext): TypedScheduledProcess<boolean>;
    setDefault(args: VerbArguments, config: CommandConfig): ScheduledProcess;
}

class SelectorParameter implements ActionParameter {
    source: string[] | ((x: RegExpMatchArray) => string | undefined) = [];
    allowMultiple = false;
    allowAll = false;
    tangibleOnly = false;
    defaultsTo: 'single' | 'all' | 'none' | 'error' = 'error';
    policy: Partial<ObjectSelectorPolicy> = {};
    searchRange?: (ag: Agent) => AObject[];
    notFoundMsg?: (ag: Agent, orig: string) => void;
    defaultsToMsg?: (ag: Agent, objs: AObject[]) => void;
    conditions: SelectorCondition[] = [];

    lastError?: () => void;
    assignment?: 'object' | 'container' | 'instrument';

    constructor(public readonly name: string, public userName?: string) {};

    *createArgument(matches: RegExpMatchArray, 
        config: CommandConfig, args: VerbArguments, context: NarrativeContext)
    {
        let query = _getQuery(matches, this.source);
        let selector: ObjectSelector;
        // ADV_UNDONE: optional not considered here?
        if (query !== undefined) {
            try {
                selector = parseObjectSelector(query, {
                    allowAll: this.allowAll,
                    restrictSingular: !this.allowMultiple,
                    context: context });
            } catch (x) {
                config.reportError(x);
                return false;
            }
        } else {
            console.log('note: arg not present: ', this.name);
            return true;
        }
        return yield* this.init(args, config, selector);
    }

    *setDefault(args: VerbArguments, config: CommandConfig) {
        if (this.name in (this.allowMultiple ? args.multi : args.single))
            return;

        let selector: ObjectSelector;
        switch (this.defaultsTo) {
        case "single":
            selector = ObjectSelector.anything;
            break;
        case "all":
            // ADV_UNDONE
            Debug.assert(this.allowMultiple);
            selector = ObjectSelector.everything;
            break;
        case "none":
            return;
        case "error":
            Debug.assert(false);
        }
        yield* this.init(args, config, selector);
    }

    *init(args: VerbArguments, config: CommandConfig, selector: ObjectSelector) {
        const condition = (obj: AObject) => {
            for (const cond of this.conditions)
                if (!cond[0](obj, args)) {
                    this.lastError = cond[1] 
                        ? () => cond[1]!(obj, config.character.agent, args) 
                        : undefined;
                    return false;
                }
            if (!this.assignment || !args.checker) return true;
            args.routineArgs[this.assignment] = obj;
            let res = args.checker.checkParameter(args.routineArgs, this.assignment);
            // console.log('called checkParameter as', this.assignment);
            if (!res) return true;
            this.lastError = () => 
                config.character.reportFailState(res!)?.submitMomentary();
            console.log('set lasterror', this.lastError);
            return false;
        };

        Object.assign(selector.policy, this.policy);
        this.conditions.push()
        selector.initialize(this.searchRange 
            ? this.searchRange.bind(undefined, config.character.agent) 
            : config.defaultRange, condition);

        let arg = new SelectorArgument(selector, this, config, args);
        if (this.allowMultiple)
            args.multi[this.name] = arg;
        else {
            let obj = yield* arg.getNext();
            if (!obj) {
                console.log('note: yield* arg.getNext() == null: ', this.name);
                return false;
            }
            args.single[this.name] = obj;
        }
        return true;
    }
}

class BooleanParameter implements ActionParameter {
    source: string[] | ((x: RegExpMatchArray) => string | undefined) = [];
    predicate: ((cap: string) => boolean) | null = null;
    defaultsTo: 'true' | 'false' | 'none' | 'error' = 'false';
    assignment?: 'state';

    constructor(public readonly name: string) {};

    *createArgument(matches: RegExpMatchArray, _: CommandConfig, args: VerbArguments) 
    {
        let query = _getQuery(matches, this.source);
        if (query !== undefined) {
            let value = this.predicate ? this.predicate(query) : true;
            args.bool[this.name] = value;
            if (this.assignment)
                args.routineArgs[this.assignment] = value;
        }
        return true;
    }

    *setDefault(args: VerbArguments, _: CommandConfig) {
        if (this.name in args.bool) return;
        switch (this.defaultsTo) {
        case "true":
            args.bool[this.name] = true;
            return;
        case "false":
            args.bool[this.name] = false;
            return;
        case "none":
            return;
        case "error":
            Debug.assert(false);
        }
    }
}

class StringParameter implements ActionParameter {
    source: string[] | ((x: RegExpMatchArray) => string | undefined) = [];
    defaultsTo: 'error' | 'none' = 'error';
    constructor(public readonly name: string) {};

    *createArgument(matches: RegExpMatchArray, _: CommandConfig, args: VerbArguments) 
    {
        let query = _getQuery(matches, this.source);
        if (query !== undefined)
            args.str[this.name] = query;
        return true;
    }

    *setDefault(args: VerbArguments, _: CommandConfig) {
        if (this.name in args.str) return;
        switch (this.defaultsTo) {
        case "none":
            return;
        case "error":
            Debug.assert(false);
        }
    }
}

interface MultiObjectArgument {
    getNext(): TypedScheduledProcess<AObject | null>;
}

export class ListObjectArgument implements MultiObjectArgument {
    #list: AObject[];
    constructor(...list: AObject[]) {
        this.#list = list;
    }

    *getNext(): TypedScheduledProcess<AObject | null> {
        return this.#list.shift() ?? null;
    }
}

class SelectorArgument implements MultiObjectArgument {
    #param: SelectorParameter;
    #selector: ObjectSelector;
    #config: CommandConfig;
    #arguments: VerbArguments;
    #done = false;

    constructor(selector: ObjectSelector, param: SelectorParameter, 
        config: CommandConfig, args: VerbArguments) 
    {
        this.#selector = selector;
        this.#param = param;
        this.#config = config;
        this.#arguments = args;
    }

    *getNext(): TypedScheduledProcess<AObject | null> {
        let obj: AObject | null = null;
        loop: while (!this.#done) {
            let y = this.#selector.yieldNext();
            let character = this.#config.character;
            switch (y.result) {
            case "end":
                this.#done = true;
                obj = null;
                break loop;
            case "subphraseEnd":
                continue;
            case "ambiguous":
                obj = yield* character.disambiguateResults(y.original, y.candidates);
                if (!obj) continue; 
                else break loop;
            case "containerAmbiguous":
                obj = yield* character.disambiguateResults(y.original, y.candidates);
                this.#selector.submitContainerSelectorDisambiguation(obj);
                continue;
            case "ok":
                obj = y.object;
                break loop;
            case "conditionNotMet":
                console.log('getNext: cond not met:', y.object, this.#param.userName);
                if (this.#param.lastError) this.#param.lastError();
                else this.#config.conditionNotMetMsg(y.object, this.#param.userName);
                break loop;
            case "noSuchObject":
            case "containerNotFound":
                console.log('getNext: no such:', y.original, this.#param.userName);
                let msg2 = this.#param.notFoundMsg;
                if (msg2) msg2(character.agent, y.original);
                else this.#config.notFoundMsg(y.original, this.#param.userName);
                break loop;
            }
        }
        if (this.#param.assignment)
            this.#arguments.routineArgs[this.#param.assignment] = obj ?? undefined;
        console.log('getNext: returning', obj?.name.value);
        return obj;
    }
}

export type VerbArguments = {
    checker?: RoutineBase,
    
    routineArgs: RoutineArguments,
    single: {[_: string]: AObject},
    multi: {[_: string]: MultiObjectArgument},
    bool: {[_: string]: boolean},
    str: {[_: string]: string}
}

Debug.makeCustomViewer<VerbArguments>(
    (obj, cfg) => (!cfg || !cfg['noshow']) 
            && typeof (obj as VerbArguments).single == 'object'
            && typeof (obj as VerbArguments).multi == 'object',
    (obj) => ['span', 
        ['span', {style: 'color: gray'}, 'verb-args '],
        ['object', {object: {
            // ADV_UNDONE: this is useless
            single: Object.keys(obj.single).length > 0 ? obj.single : undefined,
            multi: Object.keys(obj.multi).length > 0 ? obj.multi : undefined,
            bool: Object.keys(obj.bool).length > 0 ? obj.bool : undefined,
            str: Object.keys(obj.str).length > 0 ? obj.str : undefined,
            checker: obj.checker,
            routineArgs: obj.routineArgs
        }, config: {noshow: true}}]]
);

export class AVerb {
    readonly #specs: VerbSpecification;

    constructor(specs: VerbSpecification) {
        this.#specs = specs;
        Debug.assert(this.#specs.action !== undefined);
    }

    /**
     * @returns `false` if not a match for this action; `true` if failed creating and verifying arguments; the created arguments if successful.
     */
    *parseArguments(cmd: string, config: CommandConfig, context: NarrativeContext) {
        let match = this.#specs.regex?.exec(cmd);
        if (!match) return false;
        let args: VerbArguments = {
            routineArgs: {}, single: {}, multi: {}, bool: {}, str: {}};
        if (this.#specs.checker) args.checker = this.#specs.checker();
        for (let param of this.#specs.booleans)
            if (!(yield* param.createArgument(match, config, args)))
                return true;
        for (let param of this.#specs.strings)
            if (!(yield* param.createArgument(match, config, args)))
                return true;
        for (let param of this.#specs.selectors)
            if (!(yield* param.createArgument(match, config, args, context)))
                return true;
        console.log(args);
        return args;
    }

    exec(_args: Partial<VerbArguments>, config: CommandConfig) {
        let args: VerbArguments = Object.assign({
            routineArgs: {}, single: {}, multi: {}, bool: {}, str: {}}, _args);
        if (this.#specs.checker) args.checker = this.#specs.checker();
        for (let param of this.#specs.booleans)
            param.setDefault(args, config);
        for (let param of this.#specs.strings)
            param.setDefault(args, config);
        for (let param of this.#specs.selectors)
            param.setDefault(args, config);
        return this.#specs.action!(args, config.character);
    }
}

export class VerbSpecification {
    checker?: () => RoutineBase;
    selectors: SelectorParameter[] = [];
    booleans: BooleanParameter[] = [];
    strings: StringParameter[] = [];
    action?: (args: VerbArguments, char: Character) => ScheduledProcess;
    #lastParam?: ActionParameter;

    constructor(public readonly regex?: RegExp) { }

    source(
        first: string | ((x: RegExpMatchArray) => string | undefined),
        ...rest: string[]) 
    {
        Debug.assert(this.#lastParam !== undefined);
        if (typeof first == 'string')
            this.#lastParam.source = [first, ...rest];
        else
            this.#lastParam.source = first;
        return this;
    }

    singleObjParam(sourceOrName: string,
        assign?: SelectorParameter['assignment'], userName?: string) 
    {
        let param = new SelectorParameter(sourceOrName, userName);
        param.source = [sourceOrName];
        if (assign) param.assignment = assign;
        this.selectors.push(param);
        this.#lastParam = param;
        return this;
    }

    multiObjParam(sourceOrName: string,
        assign?: SelectorParameter['assignment'], userName?: string) 
    {
        let param = new SelectorParameter(sourceOrName, userName);
        param.source = [sourceOrName];
        if (assign) param.assignment = assign;
        param.allowMultiple = true;
        this.selectors.push(param);
        this.#lastParam = param;
        return this;
    }

    allowAll() {
        Debug.assert(this.selectors.length > 0);
        this.selectors[this.selectors.length - 1].allowAll = true;
        return this;
    }

    tangibleOnly() {
        Debug.assert(this.selectors.length > 0);
        this.selectors[this.selectors.length - 1].tangibleOnly = true;
        return this;
    }

    optional() {
        Debug.assert(this.#lastParam !== undefined);
        this.#lastParam.defaultsTo = 'none';
        return this;
    }

    defaultsTo(
        x: 'single' | 'all' | 'error', msg?: (ag: Agent, objs: AObject[]) => void) 
    {
        Debug.assert(this.selectors.length > 0);
        this.selectors[this.selectors.length - 1].defaultsTo = x;
        if (msg) this.selectors[this.selectors.length - 1].defaultsToMsg = msg;
        return this;
    }

    range(rng: (ag: Agent) => AObject[]) {
        Debug.assert(this.selectors.length > 0);
        this.selectors[this.selectors.length - 1].searchRange = rng;
        return this;
    }

    condition(cond: SelectorCondition[0], msg?: SelectorCondition[1]) {
        Debug.assert(this.selectors.length > 0);
        this.selectors[this.selectors.length - 1].conditions.push([cond, msg ?? null]);
        return this;
    }

    policy(p: Partial<ObjectSelectorPolicy>) {
        Debug.assert(this.selectors.length > 0);
        Object.assign(this.selectors[this.selectors.length - 1].policy, p);
        return this;
    }

    booleanParam(sourceOrName: string, 
        assign?: BooleanParameter['assignment'], pred?: (cap: string) => boolean) 
    {
        let param = new BooleanParameter(sourceOrName);
        param.source = [sourceOrName];
        if (pred) param.predicate = pred;
        if (assign) param.assignment = assign;
        this.booleans.push(param);
        this.#lastParam = param;
        return this;
    }

    stringParam(sourceOrName: string) {
        let param = new StringParameter(sourceOrName);
        param.source = [sourceOrName];
        this.strings.push(param);
        this.#lastParam = param;
        return this;
    }

    checkParams(checker: () => RoutineBase) {
        this.checker = checker;
        console.log('assigned checker', checker);
        return this;
    }

    do(act: (args: VerbArguments, char: Character) => ScheduledProcess) {
        this.action = act;
        return new AVerb(this);
    }

    link<args extends any[]>(
        routine: () => ARoutine<args>, 
        rargs: (args: VerbArguments, char: Character) => args)
    {
        this.checkParams(routine);
        this.action = function*(args, char) {
            let r = yield* routine().execDirect(char.agent, ...rargs(args, char));
            if (r.type) char.reportFailState(r)?.submitMomentary();
        };
        return new AVerb(this);
    }

    // doRoutine(act: (args: VerbArguments, char: Character) => ScheduledRoutine) {
    //     this.action = function*(args, char) {
    //         let r = yield* act(args, char);
    //         if (r.type) char.reportFailState(r);
    //     };
    //     return new AAction(this);
    // }


    linkForEach<args extends any[]>(
        param: string, routine: () => ARoutine<args>, 
        rargs: (obj: AObject, args: VerbArguments, char: Character) => readonly [...args])
    {
        this.checkParams(routine);
        this.action = function*(args, char) {
            let obj: AObject | null;
            while (obj = yield* args.multi[param].getNext()) {
                let ret = yield* routine().execDirect(
                    char.agent, ...rargs(obj, args, char));
                if (ret.type) char.reportFailState(ret)?.submitMomentary();
                if (ret.disruptive) break;
            }
        };
        return new AVerb(this);
    }

    doForEach(param: string,
        act: (obj: AObject, char: Character, args: VerbArguments) => ScheduledRoutine) {
        this.action = function* (args, char) {
            let obj: AObject | null;
            while (obj = yield* args.multi[param].getNext()) {
                let ret = yield* act(obj, char, args);
                if (ret.type) char.reportFailState(ret)?.submitMomentary();
                if (ret.disruptive) break;
            }
        };
        return new AVerb(this);
    }
}
